<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>用户会话录制与回放 - Session Replay</title>
    <!-- 
      用户会话录制与回放系统
      基于 rrweb 技术实现的前端用户行为录制和回放功能
      主要功能：
      1. 录制用户在页面上的所有操作（点击、输入、滚动等）
      2. 回放录制的用户操作，完整还原用户行为
      3. 支持样式保留，确保回放时视觉效果一致
      4. 提供友好的操作界面和状态反馈
    -->
    <style>
      /* 全局样式重置和基础设置 */
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box; /* 统一盒模型，便于布局计算 */
      }

      body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* 现代化字体栈 */
        background: linear-gradient(
          135deg,
          #667eea 0%,
          #764ba2 100%
        ); /* 渐变背景 */
        min-height: 100vh; /* 确保页面至少占满视口高度 */
        padding: 20px; /* 页面边距 */
      }

      /* 主容器样式 */
      .container {
        max-width: 1200px; /* 限制最大宽度，保持良好的阅读体验 */
        margin: 0 auto; /* 水平居中 */
        background: white; /* 白色背景 */
        border-radius: 20px; /* 圆角边框 */
        box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); /* 阴影效果 */
        overflow: hidden; /* 隐藏溢出内容 */
      }

      /* 页面头部样式 */
      .header {
        background: linear-gradient(
          135deg,
          #667eea 0%,
          #764ba2 100%
        ); /* 渐变背景 */
        color: white; /* 白色文字 */
        padding: 30px; /* 内边距 */
        text-align: center; /* 文字居中 */
      }

      .header h1 {
        font-size: 2.5em;
        margin-bottom: 10px;
        font-weight: 300;
      }

      .header p {
        font-size: 1.1em;
        opacity: 0.9;
      }

      .content {
        padding: 40px;
      }

      .section {
        margin-bottom: 40px;
        padding: 30px;
        background: #f8f9fa;
        border-radius: 15px;
        border-left: 5px solid #667eea;
      }

      .section h2 {
        color: #333;
        margin-bottom: 20px;
        font-size: 1.8em;
        font-weight: 500;
      }

      .controls {
        display: flex;
        gap: 15px;
        margin-bottom: 20px;
        flex-wrap: wrap;
      }

      /* 按钮基础样式 */
      .btn {
        padding: 12px 24px; /* 内边距 */
        border: none; /* 移除边框 */
        border-radius: 8px; /* 圆角 */
        cursor: pointer; /* 鼠标指针 */
        font-size: 16px; /* 字体大小 */
        font-weight: 500; /* 字体粗细 */
        transition: all 0.3s ease; /* 平滑过渡效果 */
        display: flex; /* 弹性布局 */
        align-items: center; /* 垂直居中 */
        gap: 8px; /* 图标和文字间距 */
      }

      /* 按钮悬停效果 */
      .btn:hover {
        transform: translateY(-2px); /* 向上移动 */
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); /* 阴影效果 */
      }

      /* 主要按钮样式（播放） */
      .btn-primary {
        background: linear-gradient(
          135deg,
          #667eea 0%,
          #764ba2 100%
        ); /* 蓝紫渐变 */
        color: white;
      }

      /* 成功按钮样式（开始录制） */
      .btn-success {
        background: linear-gradient(
          135deg,
          #56ab2f 0%,
          #a8e6cf 100%
        ); /* 绿色渐变 */
        color: white;
      }

      /* 危险按钮样式（停止录制） */
      .btn-danger {
        background: linear-gradient(
          135deg,
          #ff416c 0%,
          #ff4b2b 100%
        ); /* 红色渐变 */
        color: white;
      }

      /* 警告按钮样式（清空/暂停） */
      .btn-warning {
        background: linear-gradient(
          135deg,
          #f093fb 0%,
          #f5576c 100%
        ); /* 粉色渐变 */
        color: white;
      }

      /* 禁用按钮样式 */
      .btn:disabled {
        opacity: 0.6; /* 透明度降低 */
        cursor: not-allowed; /* 禁用鼠标指针 */
        transform: none; /* 移除变换效果 */
      }

      /* 状态显示基础样式 */
      .status {
        padding: 15px; /* 内边距 */
        border-radius: 8px; /* 圆角 */
        margin-bottom: 20px; /* 下边距 */
        font-weight: 500; /* 字体粗细 */
      }

      /* 录制中状态样式 */
      .status.recording {
        background: #d4edda; /* 浅绿背景 */
        color: #155724; /* 深绿文字 */
        border: 1px solid #c3e6cb; /* 绿色边框 */
      }

      /* 停止状态样式 */
      .status.stopped {
        background: #f8d7da; /* 浅红背景 */
        color: #721c24; /* 深红文字 */
        border: 1px solid #f5c6cb; /* 红色边框 */
      }

      /* 播放中状态样式 */
      .status.playing {
        background: #d1ecf1; /* 浅蓝背景 */
        color: #0c5460; /* 深蓝文字 */
        border: 1px solid #bee5eb; /* 蓝色边框 */
      }

      /* 回放容器样式 */
      .replay-container {
        margin-top: 20px; /* 上边距 */
        border: 2px solid #dee2e6; /* 边框 */
        border-radius: 10px; /* 圆角 */
        overflow: hidden; /* 隐藏溢出 */
        background: #fff; /* 白色背景 */
      }

      /* 回放目标区域样式 */
      .replay-target {
        width: 100%; /* 全宽 */
        height: 700px; /* 固定高度 */
        border: none; /* 无边框 */
        background: #f8f9fa; /* 浅灰背景 */
        overflow: hidden; /* 隐藏溢出 */
        position: relative; /* 相对定位 */
        padding-bottom: 85px; /* 底部内边距 */
      }

      .replay-target .rr-player {
        width: 100% !important;
        height: 100% !important;
      }

      .progress-container {
        padding: 20px;
        background: #f8f9fa;
        border-top: 1px solid #dee2e6;
      }

      .progress-bar {
        width: 100%;
        height: 8px;
        background: #e9ecef;
        border-radius: 4px;
        overflow: hidden;
        margin-bottom: 15px;
      }

      .progress-fill {
        height: 100%;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        width: 0%;
        transition: width 0.3s ease;
      }

      .time-info {
        display: flex;
        justify-content: space-between;
        align-items: center;
        font-size: 14px;
        color: #6c757d;
      }

      .events-info {
        background: #e9ecef;
        padding: 15px;
        border-radius: 8px;
        margin-top: 15px;
      }

      .events-info h3 {
        margin-bottom: 10px;
        color: #495057;
      }

      .icon {
        width: 16px;
        height: 16px;
      }

      .tech-notes {
        background: #fff3cd;
        border: 1px solid #ffeaa7;
        border-radius: 8px;
        padding: 20px;
        margin-top: 30px;
      }

      .tech-notes h3 {
        color: #856404;
        margin-bottom: 15px;
      }

      .tech-notes ul {
        color: #856404;
        padding-left: 20px;
      }

      .tech-notes li {
        margin-bottom: 8px;
      }

      @media (max-width: 768px) {
        .controls {
          flex-direction: column;
        }

        .btn {
          justify-content: center;
        }

        .header h1 {
          font-size: 2em;
        }

        .content {
          padding: 20px;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="header">
        <h1>🎬 用户会话录制与回放</h1>
        <p>基于 rrweb 技术的无感知前端录屏解决方案</p>
      </div>

      <div class="content">
        <!-- 录制控制区域 -->
        <div class="section">
          <h2>📹 录制控制</h2>
          <div class="status" id="recordStatus">
            准备就绪 - 点击开始录制按钮开始记录用户操作
          </div>

          <div class="controls">
            <button class="btn btn-success" id="startBtn">
              <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
                <path
                  fill-rule="evenodd"
                  d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
                  clip-rule="evenodd"
                ></path>
              </svg>
              开始录制
            </button>

            <button class="btn btn-danger" id="stopBtn" disabled>
              <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
                <path
                  fill-rule="evenodd"
                  d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z"
                  clip-rule="evenodd"
                ></path>
              </svg>
              停止录制
            </button>

            <button class="btn btn-warning" id="clearBtn">
              <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
                <path
                  fill-rule="evenodd"
                  d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"
                  clip-rule="evenodd"
                ></path>
                <path
                  fill-rule="evenodd"
                  d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414L7.586 12l-1.293 1.293a1 1 0 101.414 1.414L9 13.414l1.293 1.293a1 1 0 001.414-1.414L10.414 12l1.293-1.293z"
                  clip-rule="evenodd"
                ></path>
              </svg>
              清空数据
            </button>
          </div>

          <div class="events-info">
            <h3>录制信息</h3>
            <p>已录制事件数量: <span id="eventCount">0</span></p>
            <p>录制时长: <span id="recordDuration">00:00</span></p>
          </div>
        </div>

        <!-- 视频回放区域 -->
        <div class="section">
          <h2>🎥 视频回放</h2>
          <div class="status" id="playStatus">
            暂无录制数据 - 请先进行录制操作
          </div>

          <div class="controls">
            <button class="btn btn-primary" id="playBtn" disabled>
              <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
                <path
                  fill-rule="evenodd"
                  d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
                  clip-rule="evenodd"
                ></path>
              </svg>
              播放回放
            </button>

            <button class="btn btn-warning" id="pauseBtn" disabled>
              <svg class="icon" fill="currentColor" viewBox="0 0 20 20">
                <path
                  fill-rule="evenodd"
                  d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
                  clip-rule="evenodd"
                ></path>
              </svg>
              暂停
            </button>
          </div>

          <div
            class="replay-container"
            id="replayContainer"
            style="display: none"
          >
            <div id="replayTarget" class="replay-target"></div>
          </div>
        </div>

        <!-- 技术说明 -->
        <div class="tech-notes">
          <h3>🔧 技术说明</h3>
          <ul>
            <li>
              <strong>rrweb技术</strong>:
              使用开源的rrweb库实现DOM级别的录制和回放
            </li>
            <li>
              <strong>无感知录制</strong>:
              不影响页面性能，记录所有用户交互和DOM变化
            </li>
            <li>
              <strong>完整回放</strong>:
              支持鼠标移动、点击、滚动、输入等所有操作的精确回放
            </li>
            <li>
              <strong>隐私保护</strong>: 支持敏感信息遮罩，保护用户隐私数据
            </li>
            <li><strong>跨浏览器</strong>: 兼容现代浏览器，支持移动端设备</li>
          </ul>
        </div>
      </div>
    </div>

    <!-- 引入rrweb库 - 使用稳定版本 -->
    <script src="./rrweb/rrweb.min.js"></script>
    <script src="./rrweb/index.js"></script>
    <link rel="stylesheet" href="./rrweb/style.css" />
    <script>
      /**
       * DOM方法兼容性Polyfill
       * 解决rrweb库在某些浏览器环境下的兼容性问题
       * 主要解决matches、closest等方法的兼容性
       */
      ;(function () {
        /**
         * 创建安全的方法绑定函数
         * @param {Function} originalMethod - 原始方法
         * @param {Object} context - 执行上下文
         * @returns {Function} 安全包装后的方法
         */
        function createSafeMethod(originalMethod, context) {
          return function () {
            try {
              return originalMethod.apply(context || this, arguments)
            } catch (e) {
              console.warn('DOM方法调用失败:', e)
              return false
            }
          }
        }

        // 保存原生方法引用，确保兼容性
        const nativeMatches =
          Element.prototype.matches || // 标准方法
          Element.prototype.webkitMatchesSelector || // WebKit前缀
          Element.prototype.mozMatchesSelector || // Mozilla前缀
          Element.prototype.msMatchesSelector || // IE前缀
          Element.prototype.oMatchesSelector // Opera前缀

        const nativeQuerySelectorAll = Document.prototype.querySelectorAll
        const nativeGetElementsByTagName =
          Document.prototype.getElementsByTagName

        /**
         * 安全的matches方法实现
         * 用于检查元素是否匹配指定的CSS选择器
         * @param {string} selector - CSS选择器
         * @returns {boolean} 是否匹配
         */
        function safeMatches(selector) {
          try {
            // 检查this是否为有效的元素节点
            if (!this || this.nodeType !== 1 || !selector) {
              return false
            }

            // 优先使用原生方法
            if (nativeMatches) {
              return nativeMatches.call(this, selector)
            }

            // 备用实现 - 使用querySelectorAll
            const doc = this.ownerDocument || document
            if (doc && nativeQuerySelectorAll) {
              const nodes = nativeQuerySelectorAll.call(doc, selector)
              for (let i = 0; i < nodes.length; i++) {
                if (nodes[i] === this) {
                  return true
                }
              }
            }
            return false
          } catch (e) {
            console.warn('matches方法执行失败:', e)
            return false
          }
        }

        // 为Element添加matches方法
        if (!Element.prototype.matches) {
          Element.prototype.matches = safeMatches
        }

        // 为Node添加matches方法（某些情况下rrweb可能会在Node上调用）
        if (!Node.prototype.matches) {
          Node.prototype.matches = function (selector) {
            // 只在元素节点上执行匹配
            if (this && this.nodeType === 1) {
              return safeMatches.call(this, selector)
            }
            return false
          }
        }

        // 为Document和DocumentFragment添加安全的matches方法
        if (!Document.prototype.matches) {
          Document.prototype.matches = function () {
            return false
          }
        }
        if (
          typeof DocumentFragment !== 'undefined' &&
          !DocumentFragment.prototype.matches
        ) {
          DocumentFragment.prototype.matches = function () {
            return false
          }
        }

        // Element.closest polyfill
        if (!Element.prototype.closest) {
          Element.prototype.closest = function (selector) {
            try {
              if (!selector) return null

              let element = this
              while (element && element.nodeType === 1) {
                if (element.matches && element.matches(selector)) {
                  return element
                }
                element = element.parentElement || element.parentNode
                if (element && element.nodeType !== 1) {
                  break
                }
              }
              return null
            } catch (e) {
              console.warn('closest方法执行失败:', e)
              return null
            }
          }
        }

        // 增强DOM方法的错误处理
        const originalQuerySelector = Document.prototype.querySelector
        if (originalQuerySelector) {
          Document.prototype.querySelector = function (selector) {
            try {
              return originalQuerySelector.call(this, selector)
            } catch (e) {
              console.warn('querySelector失败:', e)
              return null
            }
          }
        }

        // 增强querySelectorAll的错误处理
        if (nativeQuerySelectorAll) {
          Document.prototype.querySelectorAll = function (selector) {
            try {
              return nativeQuerySelectorAll.call(this, selector)
            } catch (e) {
              console.warn('querySelectorAll失败:', e)
              return []
            }
          }
        }

        // 为所有可能的DOM节点类型添加安全的matches方法
        ;[HTMLElement, SVGElement, Element].forEach(function (Constructor) {
          if (
            typeof Constructor !== 'undefined' &&
            Constructor.prototype &&
            !Constructor.prototype.matches
          ) {
            Constructor.prototype.matches = safeMatches
          }
        })

        // 添加MutationObserver的错误处理包装
        if (typeof MutationObserver !== 'undefined') {
          const OriginalMutationObserver = window.MutationObserver
          window.MutationObserver = function (callback) {
            const safeCallback = function (mutations, observer) {
              try {
                // 确保mutations是数组且每个mutation对象都有必要的属性
                const safeMutations = mutations.filter(function (mutation) {
                  return (
                    mutation && typeof mutation === 'object' && mutation.target
                  )
                })
                return callback.call(this, safeMutations, observer)
              } catch (e) {
                console.warn('MutationObserver回调执行失败:', e)
              }
            }
            return new OriginalMutationObserver(safeCallback)
          }
          // 保持原型链
          window.MutationObserver.prototype = OriginalMutationObserver.prototype
        }
      })() // 立即执行函数结束

      /**
       * 用户会话录制与回放类
       * 基于rrweb技术实现用户操作的录制和回放功能
       */
      class SessionReplayRecorder {
        /**
         * 构造函数 - 初始化录制器
         */
        constructor() {
          // 录制相关状态
          this.events = [] // 存储录制的事件数据
          this.stopFn = null // rrweb停止录制函数
          this.isRecording = false // 是否正在录制
          this.isPlaying = false // 是否正在回放
          this.replayer = null // rrweb回放器实例
          this.startTime = null // 录制开始时间
          this.recordTimer = null // 录制计时器

          // 初始化DOM元素和事件绑定
          this.initializeElements()
          this.bindEvents()
        }

        /**
         * 初始化DOM元素引用
         * 获取页面中的控制按钮和状态显示元素
         */
        initializeElements() {
          // 录制控制相关元素
          this.startBtn = document.getElementById('startBtn') // 开始录制按钮
          this.stopBtn = document.getElementById('stopBtn') // 停止录制按钮
          this.clearBtn = document.getElementById('clearBtn') // 清空数据按钮
          this.recordStatus = document.getElementById('recordStatus') // 录制状态显示
          this.eventCount = document.getElementById('eventCount') // 事件计数显示
          this.recordDuration = document.getElementById('recordDuration') // 录制时长显示

          // 回放控制相关元素
          this.playBtn = document.getElementById('playBtn') // 播放按钮
          this.pauseBtn = document.getElementById('pauseBtn') // 暂停按钮
          this.playStatus = document.getElementById('playStatus') // 播放状态显示
          this.replayContainer = document.getElementById('replayContainer') // 回放容器
          this.replayTarget = document.getElementById('replayTarget') // 回放目标区域
        }

        /**
         * 绑定事件监听器
         * 为各个控制按钮添加点击事件处理
         */
        bindEvents() {
          this.startBtn.addEventListener('click', () => this.startRecording()) // 开始录制
          this.stopBtn.addEventListener('click', () => this.stopRecording()) // 停止录制
          this.clearBtn.addEventListener('click', () => this.clearData()) // 清空数据
          this.playBtn.addEventListener('click', () => this.playRecording()) // 播放回放
          this.pauseBtn.addEventListener('click', () => this.pauseRecording()) // 暂停回放
        }

        /**
         * 开始录制用户操作
         * 使用rrweb库开始记录页面上的所有用户交互
         */
        startRecording() {
          // 防止重复录制
          if (this.isRecording) return

          // 重置录制数据
          this.events = []
          this.isRecording = true

          try {
            // 使用rrweb开始录制 - 配置已优化以保留样式信息
            this.stopFn = rrweb.record({
              // 事件回调函数 - 每当有用户操作时被调用
              emit: (event) => {
                try {
                  this.events.push(event) // 将事件添加到数组中
                } catch (e) {
                  console.warn('事件记录失败:', e)
                }
              },
              // 基本配置 - 只阻塞外部脚本，保留样式信息
              blockSelector: 'script[src],meta,title,noscript,base',
              ignoreSelector: 'script[src],meta,title,noscript,base',
              // 确保样式和字体被正确记录
              recordCanvas: true, // 记录Canvas内容
              collectFonts: true, // 收集字体信息
              // 性能优化配置 - 限制高频事件的采样率
              sampling: {
                mousemove: 200, // 鼠标移动事件间隔200ms
                scroll: 300, // 滚动事件间隔300ms
                input: 200, // 输入事件间隔200ms
              },
              // 错误处理 - 记录警告但不中断录制
              errorHandler: (err) => {
                console.warn('rrweb警告:', err)
                return true // 继续录制
              },
            })

            // 更新UI状态
            this.updateUI('recording')
            this.startTime = Date.now() // 记录开始时间
            this.startRecordTimer() // 启动计时器

            console.log('录制已开始')
          } catch (error) {
            // 错误处理
            console.error('录制启动失败:', error)
            this.isRecording = false
            this.updateUI('error')
            alert('录制启动失败，请刷新页面重试')
          }
        }

        /**
         * 停止录制用户操作
         * 结束当前的录制会话并保存录制数据
         */
        stopRecording() {
          // 检查是否正在录制
          if (!this.isRecording) return

          try {
            // 调用rrweb的停止函数
            if (this.stopFn) {
              this.stopFn() // 停止录制
              this.stopFn = null // 清除停止函数引用
            }

            // 更新录制状态
            this.isRecording = false
            this.stopRecordTimer() // 停止计时器
            this.updateUI('stopped') // 更新UI为停止状态

            console.log(`录制已停止，共记录 ${this.events.length} 个事件`)
          } catch (error) {
            console.error('停止录制失败:', error)
          }
        }

        /**
         * 清除录制数据
         * 停止录制并清除所有已记录的数据
         */
        clearData() {
          // 如果正在录制，先停止录制
          if (this.isRecording) {
            this.stopRecording()
          }

          // 如果正在播放，先暂停播放
          if (this.isPlaying) {
            this.pauseRecording()
          }

          // 清空事件数据
          this.events = []
          this.updateUI('cleared') // 更新UI为清除状态
          this.replayContainer.style.display = 'none' // 隐藏回放容器

          console.log('录制数据已清空')
        }

        /**
         * 播放录制的用户操作
         * 使用rrweb播放器回放之前录制的用户交互
         */
        async playRecording() {
          // 检查是否有录制数据
          if (this.events.length === 0) {
            alert('没有录制数据，请先进行录制')
            return
          }

          // 防止重复播放
          if (this.isPlaying) return

          try {
            // 更新播放状态
            this.isPlaying = true
            this.updateUI('playing') // 更新UI为播放状态
            this.replayContainer.style.display = 'block' // 显示回放容器

            // 清空回放容器，准备新的播放内容
            this.replayTarget.innerHTML = ''

            // 检查rrwebPlayer是否已加载
            if (typeof rrwebPlayer === 'undefined') {
              throw new Error('rrwebPlayer未加载，请检查网络连接')
            }

            // 创建rrwebPlayer实例 - 配置已优化以支持完整的样式回放
            this.player = new rrwebPlayer({
              target: this.replayTarget, // 播放器挂载的DOM目标
              props: {
                events: this.events, // 要回放的事件数据数组
                width: this.replayTarget.offsetWidth, // 播放器宽度
                height: this.replayTarget.offsetHeight - 100, // 播放器高度，为控制栏留出空间
                autoPlay: true, // 自动开始播放
                speedOption: [0.5, 1, 2, 4, 8], // 播放速度选项
                showController: true, // 显示播放控制器
                tags: {}, // 事件标签配置
                skipInactive: true, // 跳过非活跃时间段
                showWarning: false, // 不显示警告信息
                UNSAFE_replayCanvas: true, // 启用canvas回放以支持完整样式（可能不安全但功能完整）
                // 鼠标轨迹配置
                mouseTail: {
                  duration: 500, // 轨迹持续时间
                  lineCap: 'round', // 线条端点样式
                  lineWidth: 3, // 线条宽度
                  strokeStyle: 'red', // 轨迹颜色
                },
                // 插入自定义样式规则以确保播放器正确显示
                insertStyleRules: [
                  // 确保回放容器样式正确
                  '.rr-player { width: 100% !important; height: 100% !important; }',
                  '.rr-player iframe { width: 100% !important; height: calc(100% - 40px) !important; }',
                ],
              },
            })

            console.log('rrwebPlayer已创建，事件数量:', this.events.length)

            // 监听播放完成事件 - 延迟执行以确保播放器完全初始化
            setTimeout(() => {
              const playerElement =
                this.replayTarget.querySelector('.rr-player')
              if (playerElement) {
                // 添加播放结束事件监听器
                playerElement.addEventListener('ended', () => {
                  this.isPlaying = false
                  this.updateUI('finished') // 更新UI为完成状态
                  console.log('回放已完成')
                })
              }
            }, 1000) // 等待1秒确保播放器完全加载
          } catch (error) {
            // 错误处理
            console.error('回放启动失败:', error)
            this.isPlaying = false
            this.updateUI('error') // 更新UI为错误状态
            alert(
              '回放启动失败: ' +
                error.message +
                '\n\n请检查控制台获取详细错误信息。'
            )
          }
        }

        /**
         * 暂停录制回放
         * 暂停当前正在播放的录制内容
         */
        pauseRecording() {
          // 检查是否正在播放
          if (!this.isPlaying) return

          try {
            // rrwebPlayer自带播放控制，这里只更新UI状态
            this.isPlaying = false // 更新播放状态
            this.updateUI('paused') // 更新UI为暂停状态

            console.log('回放已暂停')
          } catch (error) {
            console.error('暂停回放失败:', error)
          }
        }

        /**
         * 启动录制计时器
         * 每秒更新一次录制时长显示
         */
        startRecordTimer() {
          this.recordTimer = setInterval(() => {
            if (this.startTime) {
              const duration = Date.now() - this.startTime // 计算录制时长
              this.recordDuration.textContent = this.formatTime(duration) // 更新时长显示
            }
          }, 1000) // 每1000ms（1秒）更新一次
        }

        /**
         * 停止录制计时器
         * 清除定时器并重置引用
         */
        stopRecordTimer() {
          if (this.recordTimer) {
            clearInterval(this.recordTimer) // 清除定时器
            this.recordTimer = null // 重置引用
          }
        }

        // 移除进度跟踪方法，rrwebPlayer自带播放控制

        /**
         * 更新用户界面状态
         * 根据当前操作状态更新按钮和状态显示
         * @param {string} state - 当前状态：recording, stopped, playing, paused, finished, cleared, error
         */
        updateUI(state) {
          // 更新事件计数显示
          this.eventCount.textContent = this.events.length

          // 根据不同状态更新按钮状态和状态显示
          switch (state) {
            case 'recording': // 录制中状态
              this.startBtn.disabled = true // 禁用开始录制按钮
              this.stopBtn.disabled = false // 启用停止录制按钮
              this.clearBtn.disabled = true // 禁用清空按钮
              this.playBtn.disabled = true // 禁用播放按钮
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.recordStatus.className = 'status recording' // 设置录制状态样式
              this.recordStatus.textContent =
                '🔴 正在录制中... 请进行一些操作来测试录制功能'

              this.playStatus.className = 'status'
              this.playStatus.textContent = '录制进行中，停止录制后可进行回放'
              break

            case 'stopped': // 录制停止状态
              this.startBtn.disabled = false // 启用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = false // 启用清空按钮
              this.playBtn.disabled = this.events.length === 0 // 根据是否有数据决定播放按钮状态
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.recordStatus.className = 'status stopped' // 设置停止状态样式
              this.recordStatus.textContent =
                '⏹️ 录制已停止 - 可以开始新的录制或播放已录制内容'

              this.playStatus.className = 'status'
              this.playStatus.textContent =
                this.events.length > 0
                  ? `录制完成，共 ${this.events.length} 个事件 - 点击播放回放按钮查看录制内容`
                  : '暂无录制数据'
              break

            case 'playing': // 播放中状态
              this.startBtn.disabled = true // 禁用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = true // 禁用清空按钮
              this.playBtn.disabled = true // 禁用播放按钮
              this.pauseBtn.disabled = false // 启用暂停按钮

              this.playStatus.className = 'status playing' // 设置播放状态样式
              this.playStatus.textContent = '▶️ 正在回放录制内容...'
              break

            case 'paused': // 播放暂停状态
              this.startBtn.disabled = false // 启用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = false // 启用清空按钮
              this.playBtn.disabled = false // 启用播放按钮
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.playStatus.className = 'status'
              this.playStatus.textContent = '⏸️ 回放已暂停'
              break

            case 'finished': // 播放完成状态
              this.startBtn.disabled = false // 启用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = false // 启用清空按钮
              this.playBtn.disabled = false // 启用播放按钮
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.playStatus.className = 'status'
              this.playStatus.textContent = '✅ 回放已完成'
              break

            case 'cleared': // 数据清除状态
              this.startBtn.disabled = false // 启用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = false // 启用清空按钮
              this.playBtn.disabled = true // 禁用播放按钮（无数据可播放）
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.recordStatus.className = 'status'
              this.recordStatus.textContent = '🗑️ 数据已清空 - 准备开始新的录制'

              this.playStatus.className = 'status'
              this.playStatus.textContent = '暂无录制数据 - 请先进行录制操作'

              // 重置时间显示
              this.recordDuration.textContent = '00:00'
              this.progressFill.style.width = '0%'
              this.currentTime.textContent = '00:00'
              this.totalTime.textContent = '00:00'
              break

            case 'error': // 错误状态
              this.startBtn.disabled = false // 启用开始录制按钮
              this.stopBtn.disabled = true // 禁用停止录制按钮
              this.clearBtn.disabled = false // 启用清空按钮
              this.playBtn.disabled = this.events.length === 0 // 根据是否有数据决定播放按钮状态
              this.pauseBtn.disabled = true // 禁用暂停按钮

              this.recordStatus.className = 'status stopped'
              this.recordStatus.textContent =
                '❌ 操作失败 - 请检查控制台错误信息'
              break
          }
        }

        /**
         * 格式化时间显示
         * 将毫秒转换为分:秒格式的时间字符串
         * @param {number} ms - 毫秒数
         * @returns {string} 格式化的时间字符串 (MM:SS)
         */
        formatTime(ms) {
          const seconds = Math.floor(ms / 1000) // 转换为秒
          const minutes = Math.floor(seconds / 60) // 计算分钟
          const remainingSeconds = seconds % 60 // 计算剩余秒数

          // 返回格式化的时间字符串，确保两位数显示
          return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
            .toString()
            .padStart(2, '0')}`
        }
      }

      /**
       * 检查rrweb库是否已加载完成
       * 使用轮询方式检测库的加载状态，避免在库未加载时初始化失败
       * @returns {Promise} 加载完成时resolve，超时时reject
       */
      function checkRrwebLoaded() {
        return new Promise((resolve, reject) => {
          // 检查库是否已经加载完成
          if (typeof rrweb !== 'undefined' && rrweb.record && rrweb.Replayer) {
            console.log('rrweb库加载成功')
            resolve() // 已加载，立即resolve
          } else {
            let attempts = 0
            const maxAttempts = 50 // 最大尝试次数，5秒超时

            // 设置轮询检查
            const checkInterval = setInterval(() => {
              attempts++
              // 检查库是否加载完成
              if (
                typeof rrweb !== 'undefined' &&
                rrweb.record &&
                rrweb.Replayer
              ) {
                clearInterval(checkInterval)
                console.log('rrweb库加载成功')
                resolve() // 加载完成
              } else if (attempts >= maxAttempts) {
                // 超时处理
                clearInterval(checkInterval)
                reject(new Error('rrweb库加载超时'))
              }
            }, 100) // 每100ms检查一次
          }
        })
      }

      /**
       * 页面加载完成事件处理器
       * 等待DOM加载完成后初始化会话录制器
       * 这是整个应用的入口点，确保所有依赖都已加载完成
       */
      document.addEventListener('DOMContentLoaded', async () => {
        try {
          console.log('页面DOM加载完成，开始初始化会话录制器...')

          // 等待rrweb库完全加载完成
          // 这是异步操作，确保所有必要的库文件都已正确加载
          await checkRrwebLoaded()

          // 创建会话录制器实例并挂载到全局window对象
          // 这样可以在浏览器控制台中访问录制器进行调试
          window.sessionReplayRecorder = new SessionReplayRecorder()
          console.log('Session Replay 录制器初始化成功')
        } catch (error) {
          // 初始化失败的完整错误处理流程
          console.error('Session Replay 录制器初始化失败:', error)

          // 为用户显示友好的错误信息和解决建议
          const errorMsg = `录制器初始化失败：\n\n${error.message}\n\n可能的解决方案：\n1. 检查网络连接\n2. 刷新页面重试\n3. 检查浏览器控制台获取详细错误信息`
          alert(errorMsg)

          // 在页面UI上显示错误状态，提供视觉反馈
          const recordStatus = document.getElementById('recordStatus')
          if (recordStatus) {
            recordStatus.className = 'status'
            recordStatus.textContent = '❌ 初始化失败 - ' + error.message
          }
        }
      })
    </script>
  </body>
</html>
